/*
 * Created on 8 janv. 2007
 */
package fr.mywiki.business.versioning;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.ejb.FinderException;
import javax.ejb.ObjectNotFoundException;

import fr.mywiki.business.entry.EntryLite;
import fr.mywiki.business.entry.meta.EntryModel;
import fr.mywiki.business.entry.meta.Property;
import fr.mywiki.business.user.User;
import fr.mywiki.business.utils.ZipUtils;
import fr.mywiki.business.versioning.diff.DiffFile;
import fr.mywiki.business.versioning.interfaces.VersionManagerInterface;
import fr.mywiki.model.ejbAdapter.DataController;
import fr.mywiki.model.ejbAdapter.utils.PersistenceException;
import fr.mywiki.model.queries.DatabaseMultiversionController;

/**
 * Contains several methods to handle versions of entries.
 * 
 * @author Thomas
 */
public class VersionManager implements VersionManagerInterface {
	/** Number of versions stored in a multiversion object */
	private static final int VERSIONS_BY_MULTIVERSION = 2;
	
	/** Maximum number of solitary multiversions objects for an entry */
	private static final int MAX_NUMBER_OF_ISOLATED_MULTIVERSIONS = 3;
	
	private static VersionManager instance;

	private VersionManager() {
	}

	public static VersionManager getInstance() {
		if (instance == null) {
			instance = new VersionManager();
		}
		return instance;
	}

	/**
   * Returns the list of versions linked to an entry. They are ordered in
   * increasing order of date.
   * 
   * @param entry
   *          an <code>EntryLite</code> object.
   * @return a <code>List</code> of <code>VersionLite</code> objects.
   */
	public List getVersions(EntryLite entry) {
		Object[] array;
		try {
			array = entry.getLinks(EntryLite.LINK_VERSION_ENTRY).toArray();
		} catch (Exception e) {
			return new ArrayList();
		}
		Arrays.sort(array, getVersionsComparator());

		return Arrays.asList(array);
	}

	/**
   * Returns a comparator able to compare two versions of an entry. The way to
   * sort the versions is to compare their dates. <tt>v2</tt> is greater
   * <tt>v1</tt> if and only if <tt>v2</tt>'s date is after <tt>v1</tt>'s
   * date.
   * 
   * @return a <code>Comparator</code> object that compares
   *         <code>VersionLite</code> objects.
   */
	private static Comparator getVersionsComparator() {
		return new Comparator() {
			public int compare(Object o1, Object o2) {
				VersionLite v1 = (VersionLite) o1;
				VersionLite v2 = (VersionLite) o2;

				return v1.getDate().compareTo(v2.getDate());
			}
		};
	}

	/**
	 * Creates a new version for the entry given in parameter. The version is
	 * dated from "now", the content properties and author are given in parameters.
	 * 
	 * @param entry
	 *          a <code>EntryLite</code> object
	 * @param content
	 *          the content properties of the new version
	 * @param model
	 *          a <code>EntryModel</code> object, used to know how to build the
	 *          different version properties.
	 * @param user
	 *          the author of the new version
	 * @return a <code>VersionLite</code> object representing the created
	 *         version.
	 */
	public VersionLite addVersion(EntryLite entry, Map content,
			EntryModel model, User user) throws RemoteException,
			PersistenceException, FinderException {
		// Create the VersionLite object
		VersionLite version = (VersionLite) DataController
				.createValue(VersionLite.TYPE_ENT);
		version.addLink(VersionLite.LINK_VERSION_ENTRY, entry);
		version.setAuthor(user);
		version.setDate(new Date());
		
		// Archive the content properties : according their type
		Iterator itProperties = content.keySet().iterator();
		while(itProperties.hasNext()) {
			Property prop = (Property) itProperties.next();
			String name = prop.getName();
			String value = content.get(name).toString();
			MultiversionLite multi = archive(entry,name,value);
		}
		
		version.setMultiversion(multi.getId());
		version.update();

		// Sets the current version
		setCurrentVersion(entry,version);

		return version;
	}

	/**
	 * Puts the new <code>content</code> inside the list of contents linked to
	 * the entry figen in parameter.
	 * 
	 * @param entry
	 *          a <code>EntryLite</code> object
	 * @param content
	 *          a <code>Map</code> object
	 * @return the <code>MultiversionLite</code> object which contains the
	 *         content.
	 */
	private static MultiversionLite archive(EntryLite entry, String value, String content)
			throws RemoteException, PersistenceException, ObjectNotFoundException {
		// Get all the Multiversion objects linked to the entry and containing
		// only one version each.
		List versions = DatabaseMultiversionController
				.getIsolatedMultiversions(entry, value);
		
		if (versions.size() > MAX_NUMBER_OF_ISOLATED_MULTIVERSIONS) {
			List lites = new ArrayList(); // Contains the lites
			List texts = new ArrayList(); // Contains the texts
			
			// Create a new multiversion object : take the oldest versions
			List vfm = versions.subList(0, VERSIONS_BY_MULTIVERSION); // Versions for
																																// multiversion
			// For each version isolate the version lite and the content
			for(int i=0;i<VERSIONS_BY_MULTIVERSION;i++) {
				MultiversionLite multi = (MultiversionLite) vfm.get(i);
				texts.add(new String(multi.getContent()));
				List linked = DatabaseMultiversionController.getVersions(multi);
				// Logically, the set has only one element
				if (linked.size() == 1) {
					lites.add(linked.get(0));
				}
			}
			
			MultiversionLite multi = DatabaseMultiversionController.createMultiversion();
			
			// All the lites should be linked to the new multiversion
			Iterator itLites = lites.iterator();
			while(itLites.hasNext()) {
				VersionLite version = (VersionLite)itLites.next();
				version.setMultiversion(multi.getId());
				version.update();
			}
			
			// Then the zip file is created and the links to the versions should take
			// into account the last modifications.
			byte[] zipped = ZipUtils.compress(DiffFile.buildDiffFile(lites, texts).getBytes());
			
			multi.setContent(zipped);
			multi.setCount(new Integer(VERSIONS_BY_MULTIVERSION));
			DatabaseMultiversionController.updateMultiversion(multi);
			
			// Remove the multiversions now zipped
			for(int i=0;i<vfm.size();i++)
				((MultiversionLite)vfm.get(i)).remove();
		}
		
		// Create a new multiversion object for the new version
		MultiversionLite multi = DatabaseMultiversionController.createMultiversion();
		multi.setCount(new Integer(1));
		multi.setContent(content.getBytes());
		multi.update();
		
		return multi;
	}

	/**
   * Sets the current version of the entry to the one given in parameter.
   * 
   * @param entry
   *          a <code>EntryLite</code> object
   * @param version
   *          a <code>VersionLite</code> object.
   */
	private static void setCurrentVersion(EntryLite entry, VersionLite version)
			throws RemoteException, FinderException {
		entry.setCurrentVersion(version.getId());
		entry.update();
	}
	
	/**
	 * @see VersionManagerInterface#setCurrentVersion(VersionLite)
	 */
	public void setCurrentVersion(VersionLite version) throws RemoteException,
			FinderException {
		EntryLite entry = getEntry(version);
		setCurrentVersion(entry, version);
	}
	
	/**
   * From the decompressed content of a multiversion object, this method returns
   * the list of versions contents contained int it.
   * 
   * @param content
   *          a array of bytes, the decompressed content of the base object.
   * @return a HashMap linking the identifier of a <code>VersionLite</code>
   *         object to the complete content.
   */
	private static Map readDiffFile(byte[] content) {
		String cont = new String(content);
		DiffFile diffFile = new DiffFile(cont);
		
		return diffFile.getVersions();
	}

	/**
	 * @throws RemoteException 
	 * @see VersionManagerInterface#getVersionContent(VersionLite)
   */
	public String getVersionContent(VersionLite version) throws RemoteException {
		MultiversionLite lite = DatabaseMultiversionController
				.findMultiversionByPrimaryKey(version.getMultiversion());
		
		if (lite.getCount().intValue() > 1) {
			// The lite contains several version contents, then it is zipped
			byte[] content = ZipUtils.decompress(lite.getContent());
			// Once unzipped, the content has to be read and interpreted
			return (String) readDiffFile(content).get(version.getId());
		}else
			// One only version in the object, not zipped
			return new String(lite.getContent());
	}
	
	/**
	 * Returns the current version of an entry.
	 * 
	 * @param entry
	 *          a <code>EntryLite</code> object
	 * @return a <code>VersionLite</code> object, the one indicated in the
	 *         current version link of the entry.
	 */
	public VersionLite getCurrentVersion(EntryLite entry) throws RemoteException, FinderException {
		return (VersionLite) DataController.getValue(entry
				.getCurrentVersion(), VersionLite.TYPE_ENT);
	}
	
	/**
	 * Returns the entry linked to the specified version.
	 * 
	 * @param version
	 *          a <code>VersionLite</code> object
	 * @return a <code>EntryLite</code> object, the one which contains that
	 *         version in its list of versions.
	 */
	public EntryLite getEntry(VersionLite version) throws RemoteException,
			FinderException {
		return (EntryLite) new ArrayList(version
				.getLinks(VersionLite.LINK_VERSION_ENTRY)).get(0);
	}

	/**
	 * @see fr.mywiki.business.versioning.interfaces.VersionManagerInterface#isCurrentVersion(fr.mywiki.business.versioning.VersionLite)
	 */
	public boolean isCurrentVersion(VersionLite version) throws RemoteException, FinderException {
		EntryLite entry = getEntry(version);
		
		return version.getId().equals(entry.getCurrentVersion());
	}
	
	/**
	 * @see VersionManagerInterface#getVersionNumber(VersionLite)
	 */
	public int getVersionNumber(VersionLite version) throws RemoteException, FinderException {
		EntryLite entry = getEntry(version);
		List versions = getVersions(entry);
		return versions.indexOf(version) + 1;
	} 
}
